// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;

// This is needed due to NativeAOT which doesn't enable nullable globally yet
#nullable enable

namespace ILLink.Shared.DataFlow
{
    public enum RegionKind
    {
        Try,
        Catch,
        Filter,
        Finally
    }

    public enum ConditionKind
    {
        Unconditional,
        WhenFalse,
        WhenTrue,
        Unknown
    }

    public interface IRegion<TRegion> : IEquatable<TRegion>
    {
        RegionKind Kind { get; }
    }

    public interface IBlock<TBlock> : IEquatable<TBlock>
    {
    }

    public interface IControlFlowGraph<TBlock, TRegion>
        where TBlock : struct, IBlock<TBlock>
        where TRegion : IRegion<TRegion>
    {

        // Represents an edge in the control flow graph, from a source to a destination basic block.
        public readonly struct ControlFlowBranch : IEquatable<ControlFlowBranch>
        {
            public readonly TBlock Source;

            // Might be null in a 'throw' branch.
            public readonly TBlock? Destination;

            public readonly ConditionKind ConditionKind;

            // The finally regions exited when control flows through this edge.
            // For example:
            //
            // try {
            //     try {
            //         Source();
            //     }
            //     finally {}
            // } finally {}
            // Target();
            //
            // There will be an edge in the CRFG from the block that calls
            // Source() to the block that calls Target(), which exits both
            // finally regions.
            public readonly ImmutableArray<TRegion> FinallyRegions;

            public ControlFlowBranch(TBlock source, TBlock? destination, ImmutableArray<TRegion> finallyRegions, ConditionKind conditionKind)
            {
                Source = source;
                Destination = destination;
                FinallyRegions = finallyRegions;
                ConditionKind = conditionKind;
            }

            public bool Equals(ControlFlowBranch other)
            {
                if (!Source.Equals(other.Source))
                    return false;

                if (ConditionKind != other.ConditionKind)
                    return false;

                if (Destination == null)
                    return other.Destination == null;

                return Destination.Equals(other.Destination);
            }

            public override bool Equals(object? obj)
            {
                return obj is ControlFlowBranch other && Equals(other);
            }

            public override int GetHashCode()
            {
                return HashUtils.Combine(
                    Source.GetHashCode(),
                    Destination?.GetHashCode() ?? typeof(ControlFlowBranch).GetHashCode(),
                    ConditionKind.GetHashCode());
            }
        }

        IEnumerable<TBlock> Blocks { get; }

        TBlock Entry { get; }

        // This does not include predecessor edges for exceptional control flow into
        // catch regions or finally regions. It also doesn't include edges for non-exceptional
        // control flow from try -> finally or from catch -> finally.
        IEnumerable<ControlFlowBranch> GetPredecessors(TBlock block);

        IEnumerable<ControlFlowBranch> GetSuccessors(TBlock block);

        bool TryGetEnclosingTryOrCatchOrFilter(TBlock block, [NotNullWhen(true)] out TRegion? tryOrCatchOrFilterRegion);

        bool TryGetEnclosingTryOrCatchOrFilter(TRegion region, [NotNullWhen(true)] out TRegion? tryOrCatchOrFilterRegion);

        bool TryGetEnclosingFinally(TBlock block, [NotNullWhen(true)] out TRegion? region);

        TRegion GetCorrespondingTry(TRegion cathOrFilterOrFinallyRegion);

        IEnumerable<TRegion> GetPreviousFilters(TRegion catchOrFilterRegion);

        bool HasFilter(TRegion catchRegion);

        TBlock FirstBlock(TRegion region);

        TBlock LastBlock(TRegion region);
    }
}
